#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# (This is the BSD license, based on the template at
#   http://www.opensource.org/licenses/bsd-license.php )
# 
# Copyright (C) 2008 Thibauld Favre <tfavre@gmail.com>
# All rights reserved.
#
# Author:
#  Thibauld Favre
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 
#   * Redistributions of source code must retain the above copyright
#     notice, this list of conditions and the following disclaimer.
# 
#   * Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in
#     the documentation and/or other materials provided with the
#     distribution.
# 
#   * Neither the name of the Dalke Scientific Softare, LLC, Andrew
#     Dalke, nor the names of its contributors may be used to endorse or
#     promote products derived from this software without specific prior
#     written permission.
# 
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import datetime
import PyRSS2Gen
import feedparser
_debug=1
if _debug:
    import os
    from pprint import pprint

class SpecConformanceException(Exception): pass

def rfc3339(date):
    return date.strftime('%Y-%m-%dT%H:%M:%SZ')

class FeedSyncFactory:
    def createFromFeed(self,feed):
        d = feedparser.parse(feed)
        feed_attr = {}
        for k,v in d.feed.iteritems():
            if k=='lastbuilddate': k='lastBuildDate'
            if k=='subtitle': k='description'
            if k=='sx:sharing':
                if 'since' in v: feed_attr['since']=v['since']
                if 'until' in v: feed_attr['until']=v['until']
                if 'expires' in v: feed_attr['expires']=v['expires']
                if 'related' in v: feed_attr['related']=v['related']
            elif k.find('_detail')==-1 and k!='links':
                feed_attr[k]=v
        rss = FeedSync(**feed_attr)
        for item in d.entries:
            self.importFeedSyncItem(item,rss)
        return rss

    def importFeedSyncItem(self,item,feed):
        item_attr = {}
        for k,v in item.iteritems():
            if k=='sx:sync':
                item_attr['history'] = v['history']
                item_attr['id'] = v['id']
                item_attr['deleted'] = v.get('deleted')
                item_attr['noconflicts'] = v.get('noconflicts')
            elif k.find('_detail')==-1:
                item_attr[k]=v
        feed.addItem(**item_attr)

class FeedSync(PyRSS2Gen.RSS2):
    def __init__(
            self,
            since=None,
            until=None,
            expires=None,
            related=None,
            **args):
        self.rss_attrs['xmlns:sx']='http://feedsync.org/2007/feedsync'
        PyRSS2Gen.RSS2.__init__(self,**args)
        self.sharing_attr = self.__spec_2_2({ 'since': since, 'until': until, 'expires':expires})
        if related:
            for rel_item in related: self.__spec_2_3(rel_item)
            self.related = related
        else: self.related = []

    def addItem(self,**item_attr):
        for item in self.items:
            if item.title == item_attr['title']:
                item.undelete(item_attr['endPoint'])
                return
        self.items.append(FeedSyncItem(**item_attr))
        self.__updateUntilInfo()

    def undeleteItem(self,item_id,endPoint):
        item = None
        for it in self.items:
            if it.sync_attr['id']==item_id:
                it.undelete(endPoint)
                self.__updateUntilInfo()
                break

    # See spec 3.2 for deletion behaviour
    def deleteItem(self,item_id,endPoint):
        item = None
        for it in self.items:
            if it.sync_attr['id']==item_id:
                it.delete(endPoint)
                self.__updateUntilInfo()
                break

    def publish_extensions(self, handler):
        if self.sharing_attr:
            handler.startElement("sx:sharing", self.sharing_attr)
            for rel_item in self.related:
                PyRSS2Gen._element(handler, "sx:related", None, rel_item)
            handler.endElement("sx:sharing")

    def __updateUntilInfo(self):
        self.sharing_attr['until'] = rfc3339(datetime.datetime.now())

    def __spec_2_2(self,sharing):
        tmp = sharing.copy()
        if (tmp['until']):
            if isinstance(tmp['until'],datetime.datetime): tmp['until'] = rfc3339(tmp['until'])
        else: del tmp['until']
        if (tmp['since']):
            if isinstance(tmp['since'],datetime.datetime): tmp['since'] = rfc3339(tmp['since'])
        else: del tmp['since']
        if (tmp['expires']):
            if isinstance(tmp['expires'],datetime.datetime): tmp['expires'] = rfc3339(tmp['expires'])
        else: del tmp['expires']
        if ('since' in tmp and 'until' not in tmp): raise SpecConformanceException, 'until attribute missing in sx:sharing (spec 2.2)'
        if ('since' not in tmp and 'until' in tmp): raise SpecConformanceException, 'since attribute missing in sx:sharing (spec 2.2)'
        if (len(tmp)==0): return None
        return tmp

    def __spec_2_3(self,related):
        if ('link' not in related): raise SpecConformanceException, 'link attribute missing in sx:related (spec 2.3)'
        if ('type' not in related): raise SpecConformanceException, 'link attribute missing in sx:related (spec 2.3)'
        if (related['type'].lower()!="complete" and related['type'].lower()!="aggregated"): raise SpecConformanceException, 'incorrect link attribute value in sx:related (spec 2.3)'

class FeedSyncItem(PyRSS2Gen.RSSItem):
    def __init__(
            self,
            id,
            deleted=None,
            noconflicts=None,
            endPoint=None,
            history=None,
            **args):
        if history:
            self.history = history
            max=0
            for hist in history:
                tmp = int(hist['sequence'])
                if tmp>max: max=tmp
            updates=max
        else:
            self.history = [self.__spec_2_5({'sequence':1, 'when':datetime.datetime.now(), 'by':endPoint})]
            updates=1
        self.sync_attr = self.__spec_2_4({ 'id':id, 'updates':updates, 'deleted':deleted, 'noconflicts':noconflicts })
        self.conflicts = []
        PyRSS2Gen.RSSItem.__init__(self,**args)

    def deleted(self):
        return ('deleted' in self.sync_attr) and (self.sync_attr['deleted']=='true')

    def delete(self,endPoint):
        self.__addEvent(endPoint)
        self.sync_attr['deleted'] = 'true'

    def undelete(self,endPoint):
        self.__addEvent(endPoint)
        self.sync_attr['deleted'] = 'false'

    def publish_extensions(self,handler):
        if self.sync_attr:
            handler.startElement("sx:sync", self.sync_attr)
            for hist_attr in self.history:
                PyRSS2Gen._element(handler, "sx:history", None, hist_attr)
            if self.conflicts:
                handler.startElement("sx:conflicts",{})
                for conflicting_item in self.conflicts:
                    conflicting_item.publish(handler)
                handler.endElement("sx:conflicts")
            handler.endElement("sx:sync")

    def __addEvent(self,endPoint):
        S1 = int(self.sync_attr['updates'])
        S2 = 0
        for it in self.history:
            if it['by']==endPoint and int(it['sequence']) > S2:
                S2=int(it['sequence'])
        if S1 <= S2:
            S1=S2+1
            self.sync_attr['updates']=str(S1)
        self.history.insert(0,{'sequence':str(S1), 'when':rfc3339(datetime.datetime.now()), 'by':endPoint})

    def __spec_2_4(self,sync):
        tmp = sync.copy()
        if not tmp['id']: raise SpecConformanceException, 'id attribute missing in sx:sync (spec 2.4)'
        if not tmp['updates']: raise SpecConformanceException, 'updates attribute missing in sx:sync (spec 2.4)'
        if (not isinstance(tmp['updates'],int)): raise SpecConformanceException, 'incorrect updates attribute value in sx:sync, should be integer (spec 2.4)'
        if (tmp['updates']<1 or tmp['updates']>(2**31-1)): raise SpecConformanceException, 'updates attribute value in sx:sync is out of range (spec 2.4)'
        tmp['updates']=str(tmp['updates'])
        for k,v in tmp.items():
            if not v:
                del tmp[k]
        return tmp

    def __spec_2_5(self,history):
        tmp = history.copy()
        if not tmp['sequence']: raise SpecConformanceException, 'sequence attribute missing in sx:history (spec 2.5)'
        if (not isinstance(tmp['sequence'],int)): raise SpecConformanceException, 'incorrect sequence attribute value in sx:history, should be integer (spec 2.5)'
        if (tmp['sequence']<1 or tmp['sequence']>(2**31-1)): raise SpecConformanceException, 'sequence attribute value in sx:history is out of range (spec 2.5)'
        tmp['sequence']=str(tmp['sequence'])
        if (not tmp['when'] and not tmp['by']): raise SpecConformanceException, 'when and/or by attribute missing in sx:history (spec 2.5)'
        if tmp['when']: tmp['when'] = rfc3339(tmp['when'])
        for k,v in tmp.items():
            if not v:
                del tmp[k]
        return tmp

if __name__ == '__main__':
    import tidy
    rel_feeds = [ {'link': 'http://savemyconf.com', 'type': 'complete'} ]
    rss = FeedSync(
        title = "Device Applications Feed",
        link = "http://www.savemyconf.com",
        description = "List the applications installed on the device",
        lastBuildDate = datetime.datetime.now(),
        since = datetime.datetime(2008,6,1,20,00),
        until = datetime.datetime.now(),
        expires = datetime.datetime.now(),
        related = rel_feeds,
    )
    rss.addItem(title = "firefox-2", id="app_0", endPoint="thibs")
    rss.addItem(title = "openoffice.org-2.4",id="app_1",endPoint="thibs")
    options = dict(input_xml=1,output_xml=1,add_xml_decl=1,indent=1)
    f = open("feed1.xml","w")
    f.write(str(tidy.parseString(rss.to_xml(),**options)))
    f.close()
    f = FeedSyncFactory()
    feed = f.createFromFeed("feed1.xml")
    feed.deleteItem("app_0","thibs")
    f = open("feed2.xml","w")
    f.write(str(tidy.parseString(feed.to_xml(),**options)))
    f.close()
    f = FeedSyncFactory()
    feed = f.createFromFeed("feed2.xml")
    feed.addItem(title= "firefox-2", id="app_0",endPoint="thibs")
    f = open("feed3.xml","w")
    f.write(str(tidy.parseString(feed.to_xml(),**options)))
    f.close()
    f = FeedSyncFactory()
    feed = f.createFromFeed("feed3.xml")
    feed.deleteItem("app_0","thibs")
    f = open("feed4.xml","w")
    f.write(str(tidy.parseString(feed.to_xml(),**options)))
    f.close()
    f = FeedSyncFactory()
    feed = f.createFromFeed("feed4.xml")
    feed.addItem(title= "firefox-2", id="app_0",endPoint="thibs")
    f = open("feed5.xml","w")
    f.write(str(tidy.parseString(feed.to_xml(),**options)))
    f.close()



